package org.jfsm.core;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;

public abstract class FSM<C extends FSMContext, I extends FSMInput<C>, O extends FSMOutput> {

    public class TransitionMap extends BaseTransitionMap<C> {
        ArrayList<?> references;
        Class<C> classC;
    }

    public final class Reference<RI extends FSMInput<?>, RO extends FSMOutput> {

        private final FSM<?, RI, RO>.Instance instance;
        public final RI in;
        public RO out;

        @SuppressWarnings("unchecked")
        Reference(FSM<?, RI, RO>.Instance instance) throws FSMException {
            try {
                in = (RI) instance.getInput().getClass().newInstance();
                out = (RO) instance.getOutput().getClass().newInstance();

                in.setProxy(instance.getInput());
                out.initialize();

            } catch (Exception e) {
                throw new FSMException(e);
            }

            this.instance = instance;
        }

        void postBuild() throws FSMException {
            in.initialize();
            in.setProxy(instance.getInput());
            out = instance.getOutput();
            out.setWorker(FSM.this.in.refID, FSM.this.instance.worker);
        }

    }

    protected abstract class EH extends FSMSignalHandler<C> {
    }

    protected abstract class MH<M> extends FSMSignalMHandler<C, M> {
    }

    protected abstract class SH<M> extends FSMSignalSHandler<C, M> {
    }

    final public class Instance extends InstanceImpl<C, I> {
        Instance(FSMWorker worker, TransitionMap tm) throws FSMException {
            super(worker, tm);
            setInstance(this);
            out.initialize();
            in.setWorker(worker);

            for (Object ref : tm.references) {
                ((Reference<?, ?>) ref).postBuild();
            }
        }

        public I getInput() {
            return FSM.this.in;
        }

        O getOutput() {
            return FSM.this.out;
        }
    }

    // Fields
    public final I in;
    public final O out;
    private final Class<C> classC;
    private Instance instance;

    // Methods

    public <RI extends FSMInput<?>, RO extends FSMOutput> Reference<RI, RO> getReference(
            FSM<?, RI, RO>.Instance instance) throws FSMException {
        return new Reference<RI, RO>(instance);
    }

    @SuppressWarnings("unchecked")
    public FSM() {
        Type[] types = ((ParameterizedType) getClass().getGenericSuperclass())
                .getActualTypeArguments();
        I in = null;
        O out = null;

        try {
            in = (I) newGeneric(types[1]);
            out = (O) newGeneric(types[2]);
            in.initialize();
            classC = (Class<C>) types[0];
            in.setClassC(classC);

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        this.in = in;
        this.out = out;
    }

    protected abstract void build(TransitionMap tm);

    TransitionMap buildTM() throws FSMException {
        TransitionMap tm = new TransitionMap();
        tm.logger.setName(getClass().getSimpleName());

        int eidOffset;

        try {
            ArrayList<Reference<?, ?>> references = new ArrayList<Reference<?, ?>>();
            ArrayList<String> signalNames = new ArrayList<String>();

            eidOffset = in.numSignals;
            in.getNames("in", signalNames);

            for (Field f : getClass().getFields()) {
                if (f.getType() == Reference.class) {
                    Reference<?, ?> ref = (Reference<?, ?>) f.get(this);
                    ref.in.refID = ref.instance.getOutput().registerReference(eidOffset);
                    int numSignals = ref.out.numSignals;
                    ref.out.numSignals = eidOffset;
                    eidOffset += numSignals;
                    ref.out.getNames(f.getName(), signalNames);
                    references.add(ref);
                }
            }

            tm.classC = classC;
            tm.references = references;
            tm.logger.setSignalMap(signalNames);
            build(tm);
            tm.compile(eidOffset);
            return tm;

        } catch (Exception e) {
            throw new FSMException(e);
        }
    }

    private synchronized void setInstance(Instance instance) throws FSMException {
        if (this.instance != null)
            throw new FSMException("FSM must be a singleton");
        this.instance = instance;
    }

    private Object newGeneric(Type type) throws Exception {
        if (type instanceof ParameterizedType) {
            type = ((ParameterizedType) type).getRawType();
        }
        return ((Class<?>) type).newInstance();
    }

}
